﻿
using Utils;
using System;
using System.Collections.Concurrent;
using System.Reflection;
using System.Linq;
using System.Threading;
using Microsoft.Extensions.DependencyInjection;
using System.Threading.Tasks;

namespace Utils.JobTask
{
    /// <summary>
    /// 定时任务队列
    /// </summary>
    class JobProvider : IJobProvider
    {
        private readonly IServiceProvider serviceProvider;

        private readonly ConcurrentDictionary<string, JobInstance> jobs;

        private readonly Timer timer;

        public Action<Exception> CallException => jobTaskBuilder.CallException;

        private JobTaskBuilder jobTaskBuilder;
        public JobProvider(IServiceProvider serviceProvider)
        {
            this.serviceProvider = serviceProvider;
            this.timer = new Timer(TimerCallback, this, Timeout.Infinite, Timeout.Infinite);
            this.jobs = new ConcurrentDictionary<string, JobInstance>();
        }

        public void Run(JobTaskBuilder jobBuilder)
        {
            this.jobTaskBuilder = jobBuilder;
            InjectionFactory.GetServiceTypes()
                .Where(t => t.GetCustomAttribute<JobServiceAttribute>() != null)
                .ForEach(t => jobTaskBuilder.AddService(t));
            jobTaskBuilder.RunServices.ForEach(AppendRun);
            timer.Change(2000, 1000);
            Task.Run(async () =>
            {
                await Task.Delay(1000);
                jobBuilder.InitedList.ForEach(ExecuteDelegate);
            });
        }

        private Exception GetLast(Exception e)
        {
            if (e.InnerException != null)
                return GetLast(e.InnerException);
            return e;
        }


        private void AppendCorn(MethodInfo method, MethodInfo getAction, Type implementationType)
        {
            var cornAttr = method.GetCustomAttribute<CornAttribute>();

            if (cornAttr == null) return;

            if (method.GetParameters().Length == 0)
            {
                var @delegate = (Delegate)getAction.MakeGenericMethod(implementationType).Invoke(null, new[] { method });
                jobTaskBuilder.CronRunList.Add((@delegate, cornAttr.Cron));
            }
        }

        private void AppendAppRun(MethodInfo method, MethodInfo getAction, Type implementationType)
        {
            var appRunAttr = method.GetCustomAttribute<AppRunAttribute>();

            if (appRunAttr == null) return;

            if (method.GetParameters().Length == 0)
            {
                var @delegate = (Delegate)getAction.MakeGenericMethod(implementationType).Invoke(null, new[] { method });
                jobTaskBuilder.InitedList.Add(@delegate);
            }
        }

        private void AppendRun(Type type)
        {
            var getAction = this.GetType().GetMethod(nameof(GetAction), BindingFlags.NonPublic | BindingFlags.Static);
            using var scope = serviceProvider.CreateScope();
            foreach (var implementation in scope.ServiceProvider.GetServices(type))
            {
                var implementationType = implementation.GetType();
                var methods = implementationType.GetMethods(BindingFlags.Instance | BindingFlags.Public);
                foreach (var method in methods)
                {
                    AppendCorn(method, getAction, implementationType);
                    AppendAppRun(method, getAction, implementationType);
                }
            }
        }

        private static Action<T> GetAction<T>(MethodInfo methodInfo)
        {
            return obj =>
            {
                if (typeof(Task).IsAssignableFrom(methodInfo.ReturnType))
                {
                    var task = (Task)methodInfo.Invoke(obj, null);
                    task.GetAwaiter().GetResult();
                }
                else
                {
                    methodInfo.Invoke(obj, null);
                }
            };
        }
        private void ExecuteDelegate(Delegate @delegate)
        {
            Task.Factory.StartNew(() =>
            {
                try
                {
                    using var scope = serviceProvider.CreateScope();
                    var parameters = @delegate.Method.GetParameters().FirstOrDefault();
                    var services = scope.ServiceProvider.GetServices(parameters.ParameterType);
                    foreach (var service in services)
                    {
                        try
                        {
                            @delegate.DynamicInvoke(service);
                        }
                        catch (Exception ex)
                        {
                            CallException?.Invoke(GetLast(ex));
                        }
                    }
                }
                catch (Exception e)
                {
                    CallException?.Invoke(GetLast(e));
                }
            }, TaskCreationOptions.LongRunning);
        }


        private void TimerCallback(object obj)
        {

            var now = DateTimeOffset.Now;
            foreach ((var @delegate, var cronExpression) in jobTaskBuilder.CronRunList)
            {
                if (cronExpression.IsSatisfiedBy(now))
                {
                    ExecuteDelegate(@delegate);
                }
            }
            foreach (var (_, item) in jobs)
            {
                item.Execute(serviceProvider, CallException);
            }
        }


        public void Add(int second, int retryCount, MethodInfo methodInfo, params object[] args)
        {
            string jobName = $"{methodInfo.DeclaringType.Name}::{methodInfo.Name}";
            jobs.GetOrAdd(jobName, key => new JobInstance(methodInfo.DeclaringType, methodInfo))
                .Enqueue(new JobQueue(args, DateTime.Now.ToUnixTimestamp() + second, retryCount));
        }


        public IJobService<E> GetJob<E>()
        {
            return new JobService<E>(this);
        }

    }
}
